/*
 * SqlResources.scala
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.squela
package sql

import scala.collection.mutable.Buffer

sealed abstract trait SqlResource extends Resource {

  def fields: Seq[SqlField[_]]
}

case class Table(name: String, alias: String) extends SqlResource {
  import Table._

  val fields = Buffer[SqlField[_]]()
  def references: Seq[Table.ReferenceField[_]] = for (f <- fields; if f.isInstanceOf[Table.ReferenceField[_]]) yield f.asInstanceOf[Table.ReferenceField[_]]
  def PrimaryKeyField = fields.filter(_.isInstanceOf[PrimaryKeyField[_]])
    
  override def toString = name
    
  final def apply(index: Int) = fields(index)
  final def apply(fieldName: String) = fields find {_.name == fieldName}

  protected final def Field[T](name: String, length: Int = 0, unique: Boolean = false, optional: Boolean = false)(implicit m: scala.reflect.Manifest[T]): Field[T] = {
    addField(new Field[T](name, length, unique, optional))
  }

  protected final def Reference[T](fieldName: String, referencedField: SqlField[T],
                                   onUpdate: ConstraintAction.Value, onDelete: ConstraintAction.Value): ReferenceField[T] = {
    if(referencedField.resource == null) throw new IllegalArgumentException("Field is detached")
    addField(new ReferenceField[T](fieldName, referencedField, referencedField.resource, onUpdate, onDelete))
  }

  protected final def Reference[T](field: SqlField[T], onUpdate: ConstraintAction.Value, onDelete: ConstraintAction.Value): ReferenceField[T] = {
    if(field.resource == null) throw new IllegalArgumentException("Field is detached")
    Reference(field.resource.asInstanceOf[Table].name + field.name, field, onUpdate, onDelete)
  }

  protected def buildPrimaryKey[T <: PrimaryKeyBuilder](pkbuilder: T): T = {
//        println("Building pk of " + name)
    for (field <- pkbuilder.pkfields) {
      if(field.resource ne this) {
//                println("Referencing foreing key " + field + " from " + field.resource)
        val reference = if (pkbuilder.smartReferencesName)
          Reference(field, pkbuilder.onUpdate, pkbuilder.onDelete)
        else
          Reference(field.name, field, pkbuilder.onUpdate, pkbuilder.onDelete)

        fields -= reference
        addField(new PKReference(reference))
        field.resource = this //the referenced table is only mantained by the reference.
      } else addField(field)
    }
    return pkbuilder
  }

  def addField[T, R <: SqlField[T]](f: SqlField[T]): R = {
    f.resource = this
    fields += f
    f.asInstanceOf[R]
  }
}

object Table {

  object PrimaryKeyBuilder {
    var smartReferencesName = true;
  }

  trait PrimaryKeyBuilder {
    private[Table] val pkfields = Buffer[PrimaryKeyField[_]]()

    val smartReferencesName: Boolean = PrimaryKeyBuilder.smartReferencesName

    val onUpdate: ConstraintAction.Value = ConstraintAction.Restrict
    val onDelete: ConstraintAction.Value = ConstraintAction.Restrict

    protected final def PKField[T](name: String, length: Int = 0)(implicit table: Table, m: scala.reflect.Manifest[T]): PrimaryKeyField[T] = {
      val field = new Field(name, length, false, false)(m) with PrimaryKeyField[T]
      field.resource = table
      pkfields += field
      return field
    }
  }

  object ConstraintAction extends Enumeration {
    val Restrict = Value("RESTRICT")
    val Cascade = Value("CASCADE")
    val SetNull = Value("SET NULL")
    val NoAction = Value("NO ACTION")
  }
  class ReferenceField[T] private[Table] (fieldName: String, val field: SqlField[T], val referencedTable: Table,
                                          val onUpdate: ConstraintAction.Value, val onDelete: ConstraintAction.Value) extends FieldProxy[T] with SqlField[T] {
    override def name = fieldName
  }
  trait PrimaryKeyField[T] extends SqlField[T]
    
  class PKReference[T] private[Table] (reference: ReferenceField[T]) extends ReferenceField[T](
    reference.name, reference.field, reference.referencedTable, reference.onUpdate, reference.onDelete) with PrimaryKeyField[T]

}

abstract class Join(val joinType: String, val table1: SqlResource, val table2: SqlResource, val joinedFields: Tuple2[SqlField[_], SqlField[_]]*) extends SqlResource {
  joinedFields foreach { tuple =>
    //        if(!table1.fields.contains(tuple._1) || !table2.contains(tuple._2)) throw new IllegalArgumentException("Joined field must be of the type fieldFromTableA -> fieldDromTableB")
    def checkFieldBelongs[T](f: SqlField[T], table: SqlResource) {
      f.reference match {
        case Some(r) =>
//                    println("Found reference " + r + " for " + f + " in table " + r.resource)
          if(!table.fields.contains(r))
            throw new IllegalArgumentException("Field " + r + " from " + r.resource + " does not belong to table " + table + " in Join")
        case None =>
//                    println("Not found reference for field " + f + " in table " + f.resource)
          if(!table.fields.contains(f))
            throw new IllegalArgumentException("Field " + f + " from " + f.resource + " does not belong to table " + table + " in Join")
      }
    }
    checkFieldBelongs(tuple._1, table1)
    checkFieldBelongs(tuple._2, table2)
  }

  @inline def tableName(resource: SqlResource): String = { resource match {
      case Table(name, alias) => name + " AS " + alias
      case join: Join => join.name
    }
  }
  val name: String =  {
    val sb = new StringBuilder
    sb.append(tableName(table1) + " " + joinType + " " + tableName(table2) + " ON (")
    sb.append(joinedFields map { pair =>
        val field1 = pair._1.reference.getOrElse(pair._1: Any).asInstanceOf[SqlField[_]]
        val field2 = pair._2.reference.getOrElse(pair._2: Any).asInstanceOf[SqlField[_]]
        field1.resource.asInstanceOf[Table].alias + "." + field1.name + "=" + field2.resource.asInstanceOf[Table].alias + "." + field2.name
      } mkString ", ")
    sb.append(")")
    sb.toString
  }

  def fields: Seq[SqlField[_]] = table1.fields ++ table2.fields

}
case class LeftJoin(override val table1: SqlResource, override val table2: SqlResource,
                    override val joinedFields: Tuple2[SqlField[_], SqlField[_]]*) extends Join("LEFT JOIN", table1, table2, joinedFields: _*) {
}
case class RightJoin(override val table1: SqlResource, override val table2: SqlResource,
                     override val joinedFields: Tuple2[SqlField[_], SqlField[_]]*) extends Join("RIGHT JOIN", table1, table2, joinedFields: _*) {
}

